import _ from 'lodash';
import {
    arrayToBinaryString,
    binaryStringToArray,
    decodeBase64,
    encodeBase64,
    getKeys,
    isExpiredKey,
    keyInfo,
    stripArmor
} from 'pmcrypto';

import { listToString } from '../../../helpers/arrayHelper';
import { removeEmailAlias } from '../../../helpers/string';
import { getKeyAsUri } from '../../../helpers/key';

/* @ngInject */
function contactKey(contactDetailsModel, gettextCatalog, translator) {
    const I18N = translator(() => ({
        LANG_AND: gettextCatalog.getString('and', null, 'String separator'),
        REVOCATION_MESSAGE: gettextCatalog.getString('This key is revoked.', null, 'PGP key warning'),
        EXPIRATION_MESSAGE: gettextCatalog.getString('This key is expired.', null, 'PGP key warning'),
        userIdMismatch(keyemails) {
            return gettextCatalog.getPlural(
                keyemails.length,
                'User IDs mismatch. This key is assigned to {{emails}}.',
                'User IDs mismatch. The emails {{emails}} are assigned to this key.',
                { emails: listToString(keyemails, I18N.LANG_AND) },
                'PGP key warning'
            );
        }
    }));

    const decodeDataUri = (uri = '') => {
        const base64 = uri.substring(0, 5).toLowerCase() === 'data:' ? uri.split(',')[1] : uri;
        return binaryStringToArray(decodeBase64(base64));
    };

    /**
     * Encodes an Uint8array as base64
     * @param {Uint8Array} bytes
     * @return string base64 encoding of the given bytes
     */
    const encodeBytes = (bytes) => encodeBase64(arrayToBinaryString(bytes));

    /**
     * Tries extract the openpgp keys from a vCard KEY property.
     * @param {vCard.Property} keyProperty
     * @returns {Promise<Boolean|Object>} false in case of error otherwise an openpgp key
     */
    const parseKey = (keyProperty) => {
        const dataValue = contactDetailsModel.unescapeValue(keyProperty.valueOf().trim(), true);

        try {
            // strip data url if needed.
            const bytes = decodeDataUri(dataValue);
            // normal base 64 encoding
            return getKeys(bytes);
        } catch (e) {
            // swallow
        }

        try {
            // try armored key
            return getKeys(dataValue);
        } catch (e) {
            // swallow
        }

        return false;
    };

    /**
     * Extracts the base64 value of KEY property. In case the KEY property is already a valid pgp key in base64
     * it's returned as normal. If the KEY property is an armored pgp key the armor is stripped and the base64 is returned
     * @param {vCard.Property} keyProperty
     * @returns {Promise<String|Boolean>} false in case the key property does not contain a pgp key, a base64 encoded pgp key otherwise
     */
    const getBase64Value = async (keyProperty) => {
        const dataValue = contactDetailsModel.unescapeValue(keyProperty.valueOf().trim(), true);

        try {
            // strip data url if needed.
            const bytes = decodeDataUri(dataValue);
            // normal base 64 encoding
            await getKeys(bytes);
            // the value is already in correct format
            return encodeBytes(bytes);
        } catch (e) {
            // swallow
        }

        try {
            // try armored key
            await getKeys(dataValue);
            // just output the raw base 64
            return encodeBytes(await stripArmor(dataValue));
        } catch (e) {
            // swallow
        }

        return false;
    };

    /**
     * Returns a message if the key is revoked. Otherwise returns false.
     * @param {Object} keyInfo object generated by contactKey.keyInfo
     * @returns {String|Boolean}
     */
    const expiredKey = (keyInfo) => {
        if (!keyInfo.isExpired) {
            return false;
        }
        if (keyInfo.revocationSignatures.length) {
            return I18N.REVOCATION_MESSAGE;
        }
        return I18N.EXPIRATION_MESSAGE;
    };

    /**
     * Returns a message if the users doesn't match the email where the key will be / is added too.
     * @param users
     * @param currentEmail
     * @returns {String|Boolean}
     */
    const invalidUserId = ({ users = [] }, currentEmail) => {
        // we don't normalize anything here because enigmail / pgp also doesn't normalize it.
        const userids = users.reduce((acc, { userId = {} }) => {
            // userId can be set to null
            userId && acc.push(userId.userid);
            return acc;
        }, []);

        const keyemails = userids.map((userid) => {
            const match = /<([^>]*)>/.exec(userid);
            return match ? match[1] : userid;
        });

        if (_.intersection(keyemails.map(removeEmailAlias), [removeEmailAlias(currentEmail)]).length) {
            return false;
        }

        return I18N.userIdMismatch(keyemails);
    };

    /**
     * Calculates whether the key has properties that are invalid and the user should be warned about
     * @param {Object} keyInfo
     * @param {Object} keyObject an openpgp key object to be accessed
     * @param {String} currentEmail the email of the contact the key is / will be added too
     * @returns {String|Boolean} Returns a string if there is a message otherwise false indicating the key is valid
     */
    const invalidMessage = (keyInfo, keyObject, currentEmail) => {
        const messages = [expiredKey(keyInfo), invalidUserId(keyObject, currentEmail)].filter(Boolean);
        if (!messages.length) {
            return false;
        }
        return messages.join(' ');
    };

    /**
     * Calculates extended key info for usages in the advanced contacts modal
     * @param {String} key the key for which to retrieve information from
     * @param {String} currentEmail the email of the contact the key is / will be added too
     * @returns {Promise.<*>}
     */
    const keyInfoHelper = async (key, currentEmail) => {
        const [keyObject] = await getKeys(key);
        const result = await keyInfo(key);
        const isExpired = await isExpiredKey(keyObject);

        result.key = await getKeyAsUri(key);
        result.isExpired = isExpired;
        result.invalidMessage = invalidMessage(result, keyObject, currentEmail);

        return result;
    };

    return { parseKey, getBase64Value, keyInfo: keyInfoHelper };
}
export default contactKey;
